﻿using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication;

namespace SL.M.Net
{
    /// <summary>
    /// Represent a client-server connection manager.
    /// </summary>
    public sealed class ConnectionManager : IDisposable
    {
        X509Certificate2 _myCert;
        Stream _tcpStream;
        BinaryArchiver _archiver = new BinaryArchiver();
        SessionFlags _flags;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="hostName">The target host name. If hosting as a server, this parameter is ignored.</param>
        /// <param name="client">The TCP client object.</param>
        /// <param name="flags">The session connection flags.</param>
        /// <param name="isServer">Whether or not this instance is being used as a server.</param>
        public ConnectionManager(string hostName, TcpClient client, SessionFlags flags, bool isServer)
        {
            if (String.IsNullOrEmpty(hostName))
            {
                throw new ArgumentException("Argument cannot be null or empty.", "hostName");
            }

            if (client == null)
            {
                throw new ArgumentNullException("client");
            }

            _tcpStream = client.GetStream();
            _flags = flags;

            if (_flags.HasFlag(SessionFlags.EnableSecureChannel))
            {
                _myCert = Util.GetSslCertificate();

                SslStream sslStream = new SslStream(_tcpStream, true, RemoteCertificateValidationCallback);
                SslProtocols protocols = SslProtocols.Tls11 | SslProtocols.Tls12;
                if (isServer)
                {
                    sslStream.AuthenticateAsServer(_myCert, true, protocols, false);
                }
                else
                {
                    X509Certificate2Collection certCollection = new X509Certificate2Collection();
                    certCollection.Add(_myCert);
                    sslStream.AuthenticateAsClient(hostName, certCollection, protocols, false);
                }
                if (!sslStream.IsEncrypted)
                {
                    throw new SessionAbortedException("Encryption not enabled.");
                }
                if (!sslStream.IsMutuallyAuthenticated)
                {
                    throw new SessionAbortedException("Counter-party not authenticated.");
                }
                _tcpStream = sslStream;
            }

        }

        /// <summary>
        /// Send a frame of data.
        /// </summary>
        /// <param name="frame">The target object.</param>
        public void SendFrame(GameObject frame)
        {
            byte[] frameData = _archiver.Archive(frame);
            
            int frameSz;

            if (_flags.HasFlag(SessionFlags.EnableCompression))
            {
                using (MemoryStream deflateStream = new MemoryStream(frameData.Length))
                {
                    using (DeflateStream compressionStream = new DeflateStream(deflateStream, CompressionLevel.Fastest, true))
                    {
                        compressionStream.Write(frameData, 0, frameData.Length);
                    }
                    frameSz = (int)deflateStream.Length;
                    frameData = new byte[frameSz];
                    deflateStream.Position = 0;
                    deflateStream.Read(frameData, 0, frameSz);
                }
            }
            else
            {
                frameSz = frameData.Length;
            }

            byte[] frameSzBytes = BitConverter.GetBytes((int)frameSz);
            _tcpStream.Write(frameSzBytes, 0, frameSzBytes.Length);
            _tcpStream.Write(frameData, 0, frameData.Length);
            _tcpStream.ReadByte();
        }

        /// <summary>
        /// Receive a frame of data.
        /// </summary>
        /// <param name="frame">The target object.</param>
        public void RecieveFrame(GameObject frame)
        {
            byte[] frameSzBytes = new byte[4];
            _tcpStream.Read(frameSzBytes, 0, frameSzBytes.Length);
            int frameSz = BitConverter.ToInt32(frameSzBytes, 0);

            byte[] frameData = new byte[frameSz];

            _tcpStream.Read(frameData, 0, frameData.Length);
            if (_flags.HasFlag(SessionFlags.EnableCompression))
            {
                byte[] compressedData = frameData;
                using (MemoryStream deflateStream = new MemoryStream(compressedData))
                {
                    using (DeflateStream decompressionStream = new DeflateStream(deflateStream, CompressionMode.Decompress, true))
                    {
                        List<byte> chunks = new List<byte>(frameSz);
                        int ch;
                        while ((ch = decompressionStream.ReadByte()) >= 0)
                        {
                            chunks.Add((byte)ch);
                        }
                        frameData = new byte[chunks.Count];
                        frameSz = chunks.Count;
                        chunks.CopyTo(frameData);
                    }
                }
            }

            _archiver.Unarchive(frame, frameData);
            _tcpStream.WriteByte(0);
        }

        /// <summary>
        /// Dispose of this object.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_decompressedStream")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_compressedStream")]
        public void Dispose()
        {
            if (_archiver != null)
            {
                _archiver.Dispose();
                _archiver = null;
            }
            if (_tcpStream != null)
            {
                _tcpStream.Dispose();
                _tcpStream = null;
            }
        }

        bool RemoteCertificateValidationCallback(object sender,
            X509Certificate certificate,
            X509Chain chain,
            SslPolicyErrors sslPolicyErrors
        )
        {
            // Certificate should be valid.
            // Actually, this might cause problems later on with server authentication. That would need to be solved by creating an actual server
            // authentication certificate.
            sslPolicyErrors &= ~(SslPolicyErrors.RemoteCertificateNameMismatch);
            if (sslPolicyErrors == SslPolicyErrors.None)
            {
                chain.Build(_myCert);
                X509Certificate2 myCert = Util.GetSslCertificate();
                if (certificate.Issuer.Equals(myCert.Issuer))
                {
                    return true;
                }
            }

            return false;
        }
    }
}
